﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace SocketHttpLib
{
    public class HttpHelper
    {
        public bool DebugOutput = true;
        public string IpAddress;
        public SocketError SocketError = SocketError.Success;
        public Exception Error = null;
        public TimeSpan? TimeSpendOnDnsQuery;
        public TimeSpan? TimeSpendOnSslHandshake;
        public TimeSpan? TimeSpendOnSendRequestToResponseStart;
        public TimeSpan? TimeSpendOnContentDownload;
        public TimeSpan? TimeSpendOnConnectToServer;
        public TimeSpan TimeSpendTotal;
        TimeSpan TimeElapsedUntilLastAction;
        public Stopwatch sw = new Stopwatch();
        byte[] readBuffer = new byte[102400];
        int readOffset = -1;
        public int StatusCode { get; set; }
        internal WebHeaderCollection ResponseHeaders = new WebHeaderCollection();
        public string Method { get; set; }
        public string Url { get; set; }
        public WebProxy Proxy { get; set; }

        int? ContentLength;
        string ContentType;
        private string GetRequestHeader(string method, Uri uri, string ua, string cookies)
        {
            this.Method = method;
            this.Url = uri.ToString();
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"{method} {uri} HTTP/1.1");
            sb.AppendLine($"HOST: {uri.Host}");
            sb.AppendLine("Cache-Control: max-age=0");
            //sb.AppendLine("Upgrade-Insecure-Requests: 1");
            if (!string.IsNullOrEmpty(ua))
            {
                sb.AppendLine($"User-Agent: {ua}");
            }
            else
                sb.AppendLine("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36");
            sb.AppendLine("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8");
            sb.AppendLine("Accept-Encoding: gzip, deflate");
            sb.AppendLine("Accept-Language: zh-CN,zh;q=0.8");
            sb.AppendLine("Connection: keep-alive");
            if (cookies != null)
            {
                sb.AppendLine($"Cookie: {cookies}");
            }
            if (this.ContentLength != null)
            {
                sb.AppendLine($"Content-Length: {ContentLength}");
            }
            if (!string.IsNullOrEmpty(ContentType))
            {
                sb.AppendLine($"Content-Type: {ContentType}");
            }
            sb.AppendLine();
            return sb.ToString();
        }

        public string HttpHead(string url, string ua = null, string cookies = null)
        {
            ClearHelperState();
            var uri = new Uri(url);
            Socket socket = null;
            Stream stream = null;
            try
            {
                if (Proxy != null)
                {
                    stream = Connect(Proxy.Address, out socket);
                    stream = FromProxyStream(stream, uri);
                }
                else
                {
                    stream = Connect(uri, out socket);
                }
                var headerText = GetRequestHeader("HEAD", uri, ua, cookies);
                var headerBytes = Encoding.UTF8.GetBytes(headerText);

                stream.Write(headerBytes, 0, headerBytes.Length);
                ReadResponseHeader(stream);
                return string.Empty;
            }
            catch (SocketException ex)
            {
                this.SocketError = ex.SocketErrorCode;
                return string.Empty;
            }
            catch (Exception ex)
            {
                return string.Empty;
            }
            finally
            {
                if (sw.IsRunning)
                {
                    TimeSpendTotal = sw.Elapsed;
                    sw.Stop();
                }
                if (socket != null)
                {
                    if (socket.Connected)
                        socket.Close();
                    socket = null;
                }
            }
        }

        private void ClearHelperState()
        {
            this.lineBeginIndex = 0;
            this.lineEndIndex = 0;
            this.readOffset = -1;
            SocketError = SocketError.Success;
            TimeSpendOnDnsQuery = null;
            TimeSpendOnSslHandshake = null;
            TimeSpendOnSendRequestToResponseStart = null;
            TimeSpendOnContentDownload = null;
            TimeSpendOnConnectToServer = null;
            TimeSpendTotal = TimeSpan.Zero;
            ResponseHeaders.Clear();
            this.StatusCode = 0;
            sw.Reset();
            TimeElapsedUntilLastAction = TimeSpan.Zero;
            RawHeader = string.Empty;
            this.Method = "";
            this.Url = "";
            IpAddress = "";
        }

        private void ConsoleWriteLine(string msg)
        {
            if (DebugOutput)
                Console.WriteLine(msg);
        }

        public string HttpPost(string url, string data = null, string contentType = null, string ua = null, string cookies = null)
        {
            ClearHelperState();
            var uri = new Uri(url);
            Socket socket = null;
            try
            {
                var dataBytes = !string.IsNullOrEmpty(data) ? Encoding.UTF8.GetBytes(data) : new byte[0];
                ContentType = contentType;
                if (dataBytes.Length > 0)
                {
                    ContentLength = dataBytes.Length;
                }
                Stream stream = null;
                if (Proxy != null)
                {
                    stream = Connect(Proxy.Address, out socket);
                    stream = FromProxyStream(stream, uri);
                }
                else
                {
                    stream = Connect(uri, out socket);
                }
                var headerText = GetRequestHeader("POST", uri, ua, cookies);
                var headerBytes = Encoding.UTF8.GetBytes(headerText);

                if (dataBytes.Length > 0)
                {
                    var temp = new byte[headerBytes.Length + dataBytes.Length];
                    Array.Copy(headerBytes, temp, headerBytes.Length);
                    Array.Copy(dataBytes, 0, temp, headerBytes.Length, dataBytes.Length);
                    headerBytes = temp;
                }
                var rawText = Encoding.UTF8.GetString(headerBytes);
                stream.Write(headerBytes, 0, headerBytes.Length);
                ReadResponseHeader(stream);
                return ReadResponseBody(stream);
            }
            catch (SocketException ex)
            {
                this.SocketError = ex.SocketErrorCode;
                return string.Empty;
            }
            catch (Exception ex)
            {
                return string.Empty;
            }
            finally
            {
                if (sw.IsRunning)
                {
                    TimeSpendTotal = sw.Elapsed;
                    sw.Stop();
                }
                if (socket != null)
                {
                    if (socket.Connected)
                        socket.Close();
                    socket = null;
                }
            }
        }

        public byte[] HttpGetData(string url, string ua = null, string cookies = null)
        {
            ClearHelperState(); Socket socket = null;
            Stream stream = null;
            var ms = new MemoryStream();
            try
            {
                var uri = new Uri(url);
                if (Proxy != null)
                {
                    stream = Connect(Proxy.Address, out socket);
                    stream = FromProxyStream(stream, uri);
                }
                else
                {
                    stream = Connect(uri, out socket);
                }
                var headerText = GetRequestHeader("GET", uri, ua, cookies);
                var headerBytes = Encoding.UTF8.GetBytes(headerText);

                stream.Write(headerBytes, 0, headerBytes.Length);
                ReadResponseHeader(stream);
                ReadResponseBinaryBody(stream, ms);
            }
            catch (SocketException ex)
            {
                this.SocketError = ex.SocketErrorCode;
                this.Error = ex;
            }
            catch (Exception ex)
            {
                this.Error = ex;
            }
            finally
            {
                if (sw.IsRunning)
                {
                    TimeSpendTotal = sw.Elapsed;
                    sw.Stop();
                }
                if (socket != null)
                {
                    if (socket.Connected)
                        socket.Close();
                    socket = null;
                }
                stream?.Dispose();
            }
            return ms.ToArray();
        }
        public string HttpGet(string url, string ua = null, string cookies = null)
        {
            ClearHelperState();
            var uri = new Uri(url);
            Socket socket = null;
            Stream stream = null;
            try
            {
                if (Proxy != null)
                {
                    stream = Connect(Proxy.Address, out socket);
                    stream = FromProxyStream(stream, uri);
                }
                else
                {
                    stream = Connect(uri, out socket);
                }
                var headerText = GetRequestHeader("GET", uri, ua, cookies);
                var headerBytes = Encoding.UTF8.GetBytes(headerText);

                stream.Write(headerBytes, 0, headerBytes.Length);
                ReadResponseHeader(stream);
                return ReadResponseBody(stream);
            }
            catch (SocketException ex)
            {
                this.SocketError = ex.SocketErrorCode;
                return string.Empty;
            }
            catch (Exception ex)
            {
                return string.Empty;
            }
            finally
            {
                if (sw.IsRunning)
                {
                    TimeSpendTotal = sw.Elapsed;
                    sw.Stop();
                }
                if (socket != null)
                {
                    if (socket.Connected)
                        socket.Close();
                    socket = null;
                }
                stream?.Dispose();
            }
        }

        private Stream FromProxyStream(Stream proxyStream, Uri uri)
        {
            var temp = $"CONNECT {uri.Host}:{uri.Port} HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\r\n Proxy-Connection: Keep-Alive\r\n Proxy-Authorization: Basic YWRyZDExMTphZDgxOA== \r\n\r\n";
            var request = System.Text.Encoding.ASCII.GetBytes(temp);
            proxyStream.Write(request, 0, request.Length);

            var resp = new byte[2048];
            string tempstr = null;
            if (proxyStream.Read(resp, 0, resp.Length) > 0)
            {
                tempstr = System.Text.Encoding.UTF8.GetString(resp);
                if (tempstr.Substring(9, 3) == "200")
                {
                    return proxyStream;
                }
            }
            throw new Exception("通过代理服务器连接目标服务器失败." + tempstr);
        }

        private Stream Connect(Uri uri, out Socket socket)
        {
            IPAddress[] addressList = Dns.GetHostAddresses(uri.Host);
            StopwatchRecord(ref TimeSpendOnDnsQuery);
            if (!addressList.Any())
            {
                throw new DnsResolutionException("域名解析失败");
            }
            var ip = addressList[0];
            IpAddress = ip.ToString();
            var port = uri.Port;
            var connection = new Connection(addressList[0], uri.Port == -1 ? 80 : uri.Port);
            try
            {
                connection.ConnectAndWait(5000);
                if (connection.IsConnected)
                {
                    ConsoleWriteLine($"CONNECT {ip}:{port} - OK");
                }
            }
            catch (SocketException ex)
            {
                ConsoleWriteLine($"CONNECT {ip}:{port} {ex.SocketErrorCode}");
                throw;
            }
            catch (Exception ex)
            {
                ConsoleWriteLine($"CONNECT {ip}:{port} {ex.Message}");
                throw;
            }
            finally
            {
                StopwatchRecord(ref TimeSpendOnConnectToServer);
            }
            socket = connection.Socket;
            Stream stream = new NetworkStream(socket) { ReadTimeout = 12000, WriteTimeout = 12000 };
            if (uri.Scheme == "https" || uri.Port == 443)
            {
                stream = new SslStream(stream, false, (a, b, c, d) => { return true; });
                ((SslStream)stream).AuthenticateAsClient(uri.Host);
                StopwatchRecord(ref TimeSpendOnSslHandshake);
            }
            return stream;
        }


        private void StopwatchRecord(ref TimeSpan? action)
        {
            action = sw.Elapsed - TimeElapsedUntilLastAction;
            TimeElapsedUntilLastAction = sw.Elapsed;
        }
        public string RawHeader = string.Empty;
        private void ReadResponseHeader(Stream stream)
        {
            var line = string.Empty;
            bool firstLine = true;
            while (true)
            {
                line = ReadLine(stream);
                if (firstLine)
                {
                    firstLine = false;
                    StopwatchRecord(ref TimeSpendOnSendRequestToResponseStart);
                }
                RawHeader += line + "\r\n";
                if (line.Length == 0)
                    break;
                var match = new Regex("HTTP/1.1 (\\d+) (\\w+)").Match(line);
                if (StatusCode == 0 && match.Success)
                {
                    StatusCode = int.Parse(match.Groups[1].Value);
                    continue;
                }

                int index = line.IndexOf(':');
                if (index > 0)
                {
                    ResponseHeaders.Add(line.Substring(0, index), line.Substring(index + 2));
                }
                else
                {

                }
            }
        }
        private void ReadResponseBinaryBody(Stream stream, MemoryStream ms)
        {
            Stream responseStream = stream;
            try
            {
                bool Ischunked = ResponseHeaders["Transfer-Encoding"] != null;
                int contentLength = 0;
                if (ResponseHeaders["Content-Length"] != null)
                    contentLength = int.Parse(ResponseHeaders["Content-Length"]);
                var contentEncoding = ResponseHeaders["Content-Encoding"];
                if (contentEncoding == null)
                {
                    //没有压缩和分块传输的情况
                    if (contentLength > 0)
                    {
                        StreamReadUtil.StreamRead(responseStream, ms, 0, contentLength);
                    }
                    else
                    {
                        //如果长度为0？
                        //data = new byte[0];
                        //text = ReadStream(responseStream);
                    }
                }
                else
                {
                    if (contentEncoding.Equals("gzip"))
                    {
                        if (Ischunked)
                        {
                            //经gzip压缩并分块传输的内容，如https://www.baidu.com/
                            var pipeStream = new PipeStream(responseStream, this);
                            responseStream = new GZipStream(pipeStream, CompressionMode.Decompress, false);
                            ReadBinaryStream(responseStream, ms);
                        }
                        else
                        {
                            //经gzip压缩过的内容长度，如：http://www.meierbei.com/city
                            if (contentLength > 0)
                            {
                                var buff = new byte[contentLength];
                                int bytesRead = responseStream.Read(buff, 0, buff.Length);
                                responseStream = new GZipStream(new MemoryStream(buff, 0, bytesRead), CompressionMode.Decompress, false);
                                ReadBinaryStream(responseStream, ms);
                            }
                            else
                            {
                                responseStream.Close();
                            }
                        }
                    }
                    else if (contentEncoding.Equals("deflate"))
                    {
                        if (Ischunked)
                        {
                            var pipeStream = new PipeStream(responseStream, this);
                            responseStream = new DeflateStream(pipeStream, CompressionMode.Decompress, false);
                        }
                        else
                        {
                            if (contentLength > 0)
                            {
                                var buff = new byte[contentLength];
                                int bytesRead = responseStream.Read(buff, 0, buff.Length);
                                responseStream = new DeflateStream(new MemoryStream(buff, 0, bytesRead), CompressionMode.Decompress, false);

                            }
                            else
                            {
                                responseStream.Close();
                            }
                        }
                    }
                }
            }
            finally
            {
                responseStream.Close();
            }
            StopwatchRecord(ref TimeSpendOnContentDownload);
            TimeSpendTotal = sw.Elapsed;
            sw.Stop();
        }
        private string ReadResponseBody(Stream stream)
        {
            Stream responseStream = stream;
            var ms = new MemoryStream();
            try
            {
                bool Ischunked = ResponseHeaders["Transfer-Encoding"] != null;
                int contentLength = 0;
                if (ResponseHeaders["Content-Length"] != null)
                    contentLength = int.Parse(ResponseHeaders["Content-Length"]);
                var contentEncoding = ResponseHeaders["Content-Encoding"];
                if (contentEncoding == null)
                {
                    //没有压缩和分块传输的情况
                    if (contentLength > 0)
                    {
                        ReadStream(responseStream, contentLength, ms);
                    }
                    else
                    {
                        //如果长度为0？
                        return string.Empty;
                        //text = ReadStream(responseStream);
                    }
                }
                else
                {
                    if (contentEncoding.Equals("gzip"))
                    {
                        if (Ischunked)
                        {
                            //经gzip压缩并分块传输的内容，如https://www.baidu.com/
                            var pipeStream = new PipeStream(responseStream, this);
                            responseStream = new GZipStream(pipeStream, CompressionMode.Decompress, false);
                            ReadStream(responseStream, ms);
                        }
                        else
                        {
                            //经gzip压缩过的内容长度，如：http://www.meierbei.com/city
                            if (contentLength > 0)
                            {
                                var buff = new byte[contentLength];
                                int bytesRead = responseStream.Read(buff, 0, buff.Length);
                                responseStream = new GZipStream(new MemoryStream(buff, 0, bytesRead), CompressionMode.Decompress, false);
                                ReadStream(responseStream, ms);
                            }
                            else
                            {
                                responseStream.Close();
                            }
                        }
                    }
                    else if (contentEncoding.Equals("deflate"))
                    {
                        if (Ischunked)
                        {
                            var pipeStream = new PipeStream(responseStream, this);
                            responseStream = new DeflateStream(pipeStream, CompressionMode.Decompress, false);
                        }
                        else
                        {
                            if (contentLength > 0)
                            {
                                var buff = new byte[contentLength];
                                int bytesRead = responseStream.Read(buff, 0, buff.Length);
                                responseStream = new DeflateStream(new MemoryStream(buff, 0, bytesRead), CompressionMode.Decompress, false);

                            }
                            else
                            {
                                responseStream.Close();
                            }
                        }
                    }
                }
            }
            finally
            {
                responseStream.Close();
            }
            StopwatchRecord(ref TimeSpendOnContentDownload);
            TimeSpendTotal = sw.Elapsed;
            sw.Stop();
            var bytes = ms.ToArray();
            ms.Dispose();
            return this.Encoding.GetString(bytes);
        }

        private void ReadStream(Stream sourceStream, int length, MemoryStream ms)
        {
            StreamReadUtil.StreamRead(sourceStream, ms, 0, length);
        }

        private void ReadStream(Stream sourceStream, MemoryStream ms)
        {
            var buff = new byte[1024];
            var bytesRead = 0;
            while ((bytesRead = sourceStream.Read(buff, 0, buff.Length)) > 0)
            {
                ms.Write(buff, 0, bytesRead);
            }
        }

        private void ReadBinaryStream(Stream sourceStream, MemoryStream ms)
        {
            var buff = new byte[1024];
            var bytesRead = 0;
            while ((bytesRead = sourceStream.Read(buff, 0, buff.Length)) > 0)
            {
                ms.Write(buff, 0, bytesRead);
            }
        }


        public int StreamRead(Stream stream, MemoryStream ms, int offset, int count)
        {
            StreamReadUtil.StreamRead(stream, ms, 0, count);
            readOffset += count;
            lineBeginIndex = lineEndIndex;
            lineEndIndex = readOffset + 1;
            return count;
        }

        int lineEndIndex = 0;
        int lineBeginIndex = 0;
        internal string ReadLine(Stream stream)
        {
            while (true)
            {
                var b = stream.ReadByte();
                if (b == -1)
                {
                    return null;
                }
                readOffset++;
                while (readOffset >= readBuffer.Length)
                {
                    var buff = new byte[readBuffer.Length + 1024]; //expand
                    Array.Copy(readBuffer, buff, readBuffer.Length);
                    readBuffer = buff;
                }
                readBuffer[readOffset] = (byte)b;
                if (readOffset > 0)
                {
                    if (readBuffer[readOffset - 1] == 13 && readBuffer[readOffset] == 10) //\r\n
                    {
                        lineBeginIndex = lineEndIndex;
                        lineEndIndex = readOffset + 1;
                        break;
                    }
                }
            }
            var length = lineEndIndex - lineBeginIndex - 2;
            if (length == 0)
                return string.Empty;
            if (ResponseHeaders["Content-Type"] != null)
            {
                if (ResponseHeaders["Content-Type"].IndexOf("utf-8", StringComparison.OrdinalIgnoreCase) != -1)
                {
                    Encoding = Encoding.UTF8;
                }
                else if (ResponseHeaders["Content-Type"].IndexOf("GBK", StringComparison.OrdinalIgnoreCase) != -1)
                {
                    Encoding = Encoding.GetEncoding("gb2312");
                }
            }
            return Encoding.GetString(readBuffer, lineBeginIndex, length);
        }
        public Encoding Encoding = Encoding.Default;
    }
}
